package com.yupi.springbootinit.controller;

import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.dashscope.aigc.generation.GenerationResult;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.github.benmanes.caffeine.cache.Cache;
import com.yupi.springbootinit.annotation.AuthCheck;
import com.yupi.springbootinit.common.*;
import com.yupi.springbootinit.constant.AppConstant;
import com.yupi.springbootinit.constant.RedisConstant;
import com.yupi.springbootinit.constant.UserConstant;
import com.yupi.springbootinit.exception.BusinessException;
import com.yupi.springbootinit.exception.ThrowUtils;
import com.yupi.springbootinit.guavalimit.GuavaLimit;
import com.yupi.springbootinit.manager.ai.AIManager;
import com.yupi.springbootinit.model.dto.app.*;
import com.yupi.springbootinit.model.entity.App;
import com.yupi.springbootinit.model.entity.User;
import com.yupi.springbootinit.model.vo.AppVO;
import com.yupi.springbootinit.service.AppService;
import com.yupi.springbootinit.service.QuestionService;
import com.yupi.springbootinit.service.UserService;
import io.reactivex.Flowable;
import io.reactivex.Scheduler;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RedissonClient;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.util.DigestUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.LongAdder;


/**
 * 应用接口
 *
 * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
 * @from <a href="https://www.code-nav.cn">编程导航学习圈</a>
 */
@RestController
@RequestMapping("/app")
@Slf4j
public class AppController {

    @Resource
    private AppService appService;

    @Resource
    private UserService userService;

    @Resource
    private QuestionService questionService;

    @Resource
    private AIManager aiManager;

    @Resource
    private Cache<String, String> localCache;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private RedissonClient redissonClient;

    @Resource
    private Scheduler vipScheduler;

    // region systemContext
    private static final String systemContext = "你是一位严谨的出题专家，我会给你如下信息：\n" +
            "```\n" +
            "应用名称，\n" +
            "【【【应用描述】】】，\n" +
            "应用类别，\n" +
            "要生成的题目数，\n" +
            "每个题目的选项数\n" +
            "```\n" +
            "\n" +
            "请你根据上述信息，按照以下步骤来出题：\n" +
            "1. 要求：题目和选项尽可能地短，题目不要包含序号，每题的选项数以我提供的为主，题目不能重复\n" +
            "2. 严格按照下面的 json 格式输出题目和选项\n" +
            "\n" +
            "[{\"options\":[{\"value\":\"选项内容\",\"key\":\"A\"},{\"value\":\"\",\"key\":\"B\"}],\"title\":\"题目标题\"}]\n" +
            "\n" +
            "title 是题目，options 是选项，每个选项的 key 按照英文字母序（比如 A、B、C、D）以此类推，value 是选项内容\n" +
            "3. 检查题目是否包含序号，若包含序号则去除序号\n" +
            "4. 返回的题目列表格式必须为 JSON 数组且内容只有JSON数组\n";
    // endregion


    // region 增删改查

    /**
     * 创建应用
     *
     * @param appAddRequest
     * @param request
     * @return
     */
    @PostMapping("/add")
    public BaseResponse<Long> addApp(@RequestBody AppAddRequest appAddRequest, HttpServletRequest request) {
        ThrowUtils.throwIf(appAddRequest == null, ErrorCode.PARAMS_ERROR);
        // todo 在此处将实体类和 DTO 进行转换
        App app = new App();
        BeanUtils.copyProperties(appAddRequest, app);
        // 数据校验
        appService.validApp(app, true);
        // todo 填充默认值
        User loginUser = userService.getLoginUser(request);
        app.setUserId(loginUser.getId());
        // 写入数据库
        boolean result = appService.save(app);
        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
        // 返回新写入的数据 id
        long newAppId = app.getId();
        return ResultUtils.success(newAppId);
    }

    /**
     * 删除应用
     *
     * @param deleteRequest
     * @param request
     * @return
     */
    @PostMapping("/delete")
    public BaseResponse<Boolean> deleteApp(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) {
        if (deleteRequest == null || deleteRequest.getId() <= 0) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        User user = userService.getLoginUser(request);
        long id = deleteRequest.getId();
        // 判断是否存在
        App oldApp = appService.getById(id);
        ThrowUtils.throwIf(oldApp == null, ErrorCode.NOT_FOUND_ERROR);
        // 仅本人或管理员可删除
        if (!oldApp.getUserId().equals(user.getId()) && !userService.isAdmin(request)) {
            throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
        }
        // 操作数据库
        boolean result = appService.removeById(id);
        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
        return ResultUtils.success(true);
    }

    /**
     * 更新应用（仅管理员可用）
     *
     * @param appUpdateRequest
     * @return
     */
    @PostMapping("/update")
    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
    public BaseResponse<Boolean> updateApp(@RequestBody AppUpdateRequest appUpdateRequest) {
        if (appUpdateRequest == null || appUpdateRequest.getId() <= 0) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        // todo 在此处将实体类和 DTO 进行转换
        App app = new App();
        BeanUtils.copyProperties(appUpdateRequest, app);
        // 数据校验
        appService.validApp(app, false);
        // 判断是否存在
        long id = appUpdateRequest.getId();
        App oldApp = appService.getById(id);
        ThrowUtils.throwIf(oldApp == null, ErrorCode.NOT_FOUND_ERROR);
        // 操作数据库
        boolean result = appService.updateById(app);
        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
        return ResultUtils.success(true);
    }

    /**
     * 根据 id 获取应用（封装类）
     *
     * @param id
     * @return
     */
    @GetMapping("/get/vo")
    public BaseResponse<AppVO> getAppVOById(long id, HttpServletRequest request) {
        ThrowUtils.throwIf(id <= 0, ErrorCode.PARAMS_ERROR);
        // 查询数据库
        App app = appService.getById(id);
        ThrowUtils.throwIf(app == null, ErrorCode.NOT_FOUND_ERROR);
        // 获取封装类
        return ResultUtils.success(appService.getAppVO(app, request));
    }

    /**
     * 分页获取应用列表（仅管理员可用）
     *
     * @param appQueryRequest
     * @return
     */
    @PostMapping("/list/page")
    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
    public BaseResponse<Page<App>> listAppByPage(@RequestBody AppQueryRequest appQueryRequest) {
        long current = appQueryRequest.getCurrent();
        long size = appQueryRequest.getPageSize();
        // 查询数据库
        Page<App> appPage = appService.page(new Page<>(current, size),
                appService.getQueryWrapper(appQueryRequest));
        return ResultUtils.success(appPage);
    }

    /**
     * 分页获取应用列表（封装类）
     *
     * @param appQueryRequest
     * @param request
     * @return
     */
    @PostMapping("/list/page/vo")
    public BaseResponse<Page<AppVO>> listAppVOByPage(@RequestBody AppQueryRequest appQueryRequest,
                                                     HttpServletRequest request) {
        long current = appQueryRequest.getCurrent();
        long size = appQueryRequest.getPageSize();
        // 限制爬虫
        ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
        // 压缩查询条件
        String md5 = DigestUtils.md5DigestAsHex(appQueryRequest.toString().getBytes());
        // 本地缓存
        String key = AppConstant.APP_LIST_PAGE_VO_KEY + md5;
        String cache = localCache.getIfPresent(key);
        if (StrUtil.isNotBlank(cache)) {
            Page page = JSONUtil.toBean(cache, Page.class);
            return ResultUtils.success(page);
        }
        // 防止缓存穿透
        ThrowUtils.throwIf("".equals(cache), ErrorCode.NOT_FOUND_ERROR);
        // // bloomFilter 防止缓存穿透
        // if(!bloomFilter.contains(key)) {
        //     throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
        // }
        // 分布式缓存
        ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();
        cache = valueOperations.get(key);
        if (StrUtil.isNotBlank(cache)) {
            Page page = JSONUtil.toBean(cache, Page.class);
            // 写入本地缓存
            localCache.put(key, cache);
            return ResultUtils.success(page);
        }
        ThrowUtils.throwIf("".equals(cache), ErrorCode.NOT_FOUND_ERROR);
        // 查询数据库
        Page<App> appPage = appService.page(new Page<>(current, size),
                appService.getQueryWrapper(appQueryRequest));

        // 随机时间 防止 缓存雪崩
        int randomTTL = RedisConstant.THIRTY_TTL + RandomUtil.randomInt(1, 10);
        if (appPage == null) {
            //     缓存空值 防止缓存穿透
            localCache.put(key, "");
            valueOperations.set(key, "", randomTTL, TimeUnit.MINUTES);
        }
        Page<AppVO> appVOPage = appService.getAppVOPage(appPage, request);
        // 缓存结果值
        localCache.put(key, JSONUtil.toJsonStr(appVOPage));
        valueOperations.set(key, JSONUtil.toJsonStr(appVOPage), randomTTL, TimeUnit.MINUTES);
        // 获取封装类
        return ResultUtils.success(appVOPage);
    }

    /**
     * 分页获取当前登录用户创建的应用列表
     *
     * @param appQueryRequest
     * @param request
     * @return
     */
    @PostMapping("/my/list/page/vo")
    public BaseResponse<Page<AppVO>> listMyAppVOByPage(@RequestBody AppQueryRequest appQueryRequest,
                                                       HttpServletRequest request) {
        ThrowUtils.throwIf(appQueryRequest == null, ErrorCode.PARAMS_ERROR);
        // 补充查询条件，只查询当前登录用户的数据
        User loginUser = userService.getLoginUser(request);
        appQueryRequest.setUserId(loginUser.getId());
        long current = appQueryRequest.getCurrent();
        long size = appQueryRequest.getPageSize();
        // 限制爬虫
        ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
        // 查询数据库
        Page<App> appPage = appService.page(new Page<>(current, size),
                appService.getQueryWrapper(appQueryRequest));
        // 获取封装类
        return ResultUtils.success(appService.getAppVOPage(appPage, request));
    }

    /**
     * 编辑应用（给用户使用）
     *
     * @param appEditRequest
     * @param request
     * @return
     */
    @PostMapping("/edit")
    public BaseResponse<Boolean> editApp(@RequestBody AppEditRequest appEditRequest, HttpServletRequest request) {
        if (appEditRequest == null || appEditRequest.getId() <= 0) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        // todo 在此处将实体类和 DTO 进行转换
        App app = new App();
        BeanUtils.copyProperties(appEditRequest, app);
        // 数据校验
        appService.validApp(app, false);
        User loginUser = userService.getLoginUser(request);
        // 判断是否存在
        long id = appEditRequest.getId();
        App oldApp = appService.getById(id);
        ThrowUtils.throwIf(oldApp == null, ErrorCode.NOT_FOUND_ERROR);
        // 仅本人或管理员可编辑
        if (!oldApp.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) {
            throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
        }
        // 操作数据库
        boolean result = appService.updateById(app);
        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
        return ResultUtils.success(true);
    }

    // endregion


    /**
     * 审核应用
     */
    @PostMapping("/review")
    @SaCheckLogin
    public BaseResponse<Boolean> reviewApp(@RequestBody ReviewRequest reviewRequest) {
        ThrowUtils.throwIf(reviewRequest == null, ErrorCode.PARAMS_ERROR);
        Long id = reviewRequest.getId();
        ThrowUtils.throwIf(id == null || id <= 0, ErrorCode.PARAMS_ERROR);
        appService.reviewApp(reviewRequest);
        return ResultUtils.success(true);
    }

    @GetMapping("/ai_generate/sse")
    @GuavaLimit(key = "testSSE", permitsPerSecond = 1, msg = "系统繁忙,请稍后再试,哈哈")
    public SseEmitter aiGenerateQuestionSSE(AiGenerateQuestionRequest aiGenerateQuestionRequest) {
        ThrowUtils.throwIf(aiGenerateQuestionRequest == null, ErrorCode.PARAMS_ERROR);
        // SSE 永不过期
        SseEmitter sseEmitter = new SseEmitter(0L);
        // // AI 生成
        // 流式返回
        String prompt = appService.aiGenerateQuestionPrompt(aiGenerateQuestionRequest);
        Flowable<GenerationResult> flowable = aiManager.callWithMessage(prompt, systemContext);
        LongAdder longAdder = new LongAdder();
        StringBuffer stringBuffer = new StringBuffer();
        // 创建线程池
        flowable.observeOn(vipScheduler)
                .map(chunk -> chunk.getOutput().getChoices().get(0).getMessage().getContent())
                .map(chunk -> chunk.replaceAll("\\s", ""))
                .flatMap(chunk -> {
                    List<Character> characters = new ArrayList<>();
                    for (char c : chunk.toCharArray()) {
                        characters.add(c);
                    }
                    return Flowable.fromIterable(characters);
                }).doOnNext(ch -> {
                    if (ch == '{') {
                        longAdder.increment();
                    }
                    if (longAdder.intValue() > 0) {
                        stringBuffer.append(ch);
                    }
                    if (ch == '}') {
                        longAdder.decrement();
                        if (longAdder.intValue() == 0) {
                            stringBuffer.append(ch);
                            //     发送消息
                            sseEmitter.send(stringBuffer.toString());
                            //     清空stringBuffer
                            stringBuffer.delete(0, stringBuffer.length());
                        }
                    }
                }).doOnComplete(sseEmitter::complete)
                .doOnError(sseEmitter::completeWithError).subscribe();

        // 截取字符串 容错增加
        return sseEmitter;
    }


    @GetMapping("/test/sse")
    @GuavaLimit(key = "testSSE", permitsPerSecond = 1, msg = "系统繁忙,请稍后再试,哈哈")
    public SseEmitter testSSE(String prompt) {
        StpUtil.checkLogin();
        SseEmitter sseEmitter = new SseEmitter(0L);
        // 获取线程池
        Flowable<GenerationResult> flowable = aiManager.callWithMessage(prompt, "你是一个知识渊博的人");
        flowable.observeOn(vipScheduler)
                .doOnNext(msg -> {
                    sseEmitter.send(msg.getOutput().getChoices().get(0).getMessage().getContent());
                })
                .doOnComplete(sseEmitter::complete)
                .doOnError(e -> {
                    log.error("error", e);
                })
                .subscribe();
        return sseEmitter;
    }

}
